部署背景
当前 MySQL 使用 Deployment 部署在 k3s-master 节点,存在以下问题:
Deployment 不适合有状态服务
固定调度在 master 节点,应分配到 worker 节点
缺乏稳定的网络标识
目标:使用 StatefulSet + Longhorn 重新部署,支持持久化存储和动态调度。
当前环境
项目
值
K3s 版本
v1.34.5+k3s1
存储
Longhorn (RWO)
网络插件
Flannel (10.42.0.0/16)
节点
6 节点 (1 amd64 + 5 arm64)
多层备份策略
为确保数据安全,采用 3 层备份:
层 1: SQL 逻辑备份
1 2 kubectl exec -n mysql mysql-55bfb88477-fd486 -- \ mysqldump -u root -p'{password}' gitea > /mnt/nfs/mysql-gitea-backup.sql
位置:/mnt/nfs/mysql-gitea-backup.sql
大小:约几百 KB
优点:可读性好,恢复简单
层 2: 数据文件物理备份
1 2 3 kubectl exec -n mysql mysql-55bfb88477-fd486 -- \ tar -czf /tmp/mysql-data.tar.gz -C /var/lib/mysql . \ && kubectl cp -n mysql mysql-55bfb88477-fd486:/tmp/mysql-data.tar.gz /mnt/nfs/mysql-data.tar.gz
位置:/mnt/nfs/mysql-data.tar.gz
大小:约几百 MB
优点:包含所有表、索引、配置
层 3: 保留原 PVC 和 Deployment
不删除原有资源,等新 StatefulSet 验证通过后再清理
关键注意事项
1. 镜像版本
使用固定版本而非浮动标签,避免 小版本迭代导致兼容性问题:
2. MySQL 初始化时序
数据恢复必须等待 MySQL 完全初始化完成,否则会导致恢复失败。
3. Gitea 连接
当前 Gitea 使用 ClusterIP Service(mysql.mysql.svc.cluster.local:3306),迁移后 Service 不变,无需修改应用配置。
部署步骤
Step 0: 校验 Secret(前置检查)
1 2 3 4 5 6 7 kubectl get secret mysql-secret -n mysql -o yaml kubectl get secret mysql-secret -n mysql -o jsonpath='{.data.MYSQL_ROOT_PASSWORD}' | base64 -d kubectl get secret mysql-secret -n mysql -o jsonpath='{.data.MYSQL_DATABASE}' | base64 -d kubectl get secret mysql-secret -n mysql -o jsonpath='{.data.MYSQL_USER}' | base64 -d
必需字段:MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD
如缺失或错误,需提前修复
Step 1: 验证原 MySQL 正常
1 kubectl exec -n mysql mysql-55bfb88477-fd486 -- mysqladmin ping -u root -p'{password}'
Step 2: 执行 3 层备份
层 1:SQL 导出到 /mnt/nfs/mysql-gitea-backup.sql
层 2:数据文件到 /mnt/nfs/mysql-data.tar.gz
验证备份文件存在
Step 3: 创建 StatefulSet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 apiVersion: v1 kind: Service metadata: name: mysql-headless namespace: mysql spec: clusterIP: None ports: - port: 3306 name: mysql selector: app: mysql --- apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql namespace: mysql spec: serviceName: mysql-headless replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: node-role.kubernetes.io/worker operator: Exists containers: - name: mysql image: mysql:8.0.36 envFrom: - secretRef: name: mysql-secret env: - name: TZ value: Asia/Shanghai volumeMounts: - name: data mountPath: /var/lib/mysql - name: mysql-log mountPath: /var/log/mysql volumes: - name: mysql-log emptyDir: {} livenessProbe: exec: command: - mysqladmin - ping - -h - 127.0 .0 .1 initialDelaySeconds: 60 periodSeconds: 30 readinessProbe: exec: command: - mysqladmin - ping - -h - 127.0 .0 .1 initialDelaySeconds: 30 periodSeconds: 10 resources: requests: cpu: 200m memory: 512Mi limits: cpu: 1000m memory: 1Gi volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 20Gi
特点:
使用 VolumeClaimTemplate 自动创建新 PVC
节点亲和性:优先调度到 worker 节点
健康检查:liveness + readiness probes
Step 4: 创建 NodePort Service
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: mysql namespace: mysql spec: type: NodePort ports: - port: 3306 nodePort: 30306 name: mysql selector: app: mysql
端口:30306
局域网可访问:192.168.2.x:30306
Step 5: 等待 MySQL 初始化完成
1 2 3 4 5 kubectl wait --for =condition=ready pod/mysql-0 -n mysql --timeout =300s kubectl exec -n mysql mysql-0 -- mysqladmin ping -u root -p'{password}'
Step 6: 恢复数据到新 Pod
1 2 3 4 5 cat /mnt/nfs/mysql-gitea-backup.sql | kubectl exec -i mysql-0 -n mysql -- mysql -u root -p'{password}' giteakubectl exec -n mysql mysql-0 -- mysql -u root -p'{password}' gitea -e "SHOW TABLES;"
Step 7: 验证
检查 Pod 状态:kubectl get pod mysql-0 -n mysql -o wide
检查服务:kubectl get svc mysql -n mysql
测试连接:mysql -h 192.168.2.40 -P 30306 -u root -p'{password}'
验证 gitea 应用连接:检查应用日志无错误
Step 8: 稳定运行观察
1 2 3 watch -n 10 'kubectl get pod mysql-0 -n mysql -o wide'
Step 9: 清理旧资源
1 2 kubectl delete deployment mysql -n mysql
验证清单
立即验证(迁移后)
验证项
方式
Pod 运行状态
kubectl get pod mysql-0 -n mysql
MySQL 初始化完成
mysqladmin ping 成功
数据恢复成功
SHOW TABLES; 有表
局域网访问
mysql -h 192.168.2.x -P 30306 -u root -p
延迟验证(24 小时后)
验证项
方式
Pod 运行稳定
24 小时内无重启
gitea 连接正常
检查 gitea Pod 日志无连接错误
回退方案
如有问题,执行:
1 2 3 4 5 kubectl delete statefulset mysql -n mysql kubectl apply -f /mnt/nfs/mysql-deployment.yaml
问题修复记录
1. Readiness Probe 报错
问题 :readiness probe 失败,报错 Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'
原因 :探针使用 -h localhost(socket 连接),容器启动时 socket 文件未就绪
修复 :将探针改为 TCP 连接
1 2 3 4 5 6 7 - -h - localhost - -h - 127.0 .0 .1
2. 局域网客户端连接报错
问题 :Sequel Pro 客户端连接报错 Authentication plugin 'caching_sha2_password' cannot be loaded
原因 :MySQL 8.0 默认认证插件为 caching_sha2_password,旧客户端(如 Sequel Pro)不兼容
修复 :修改用户认证插件为 mysql_native_password
1 2 3 4 5 6 7 8 9 10 11 12 kubectl exec -n mysql mysql-0 -- mysql -h 127.0.0.1 -u root -p'{password}' -e " ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '{password}'; ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '{password}'; FLUSH PRIVILEGES; " kubectl exec -n mysql mysql-0 -- mysql -h 127.0.0.1 -u root -p'{password}' -e " ALTER USER 'gitea'@'%' IDENTIFIED WITH mysql_native_password BY '{password}'; FLUSH PRIVILEGES; "
验证 :
1 kubectl exec -n mysql mysql-0 -- mysql -h 127.0.0.1 -u root -p'{password}' -e "SELECT user, host, plugin FROM mysql.user WHERE user IN ('root', 'gitea');"
相关文件
备份文件:/mnt/nfs/mysql-gitea-backup.sql
备份文件:/mnt/nfs/mysql-data.tar.gz
StatefulSet 配置:/mnt/nfs/mysql-statefulset.yaml
Deployment 配置:/mnt/nfs/mysql-deployment.yaml
Secret:mysql-secret(命名空间:mysql)